Skip to content

fix: detect provider-level failures in generic webhook notifications (#2352)#2356

Merged
kmendell merged 2 commits intogetarcaneapp:mainfrom
GiulioSavini:fix/generic-webhook-response-validation
Apr 12, 2026
Merged

fix: detect provider-level failures in generic webhook notifications (#2352)#2356
kmendell merged 2 commits intogetarcaneapp:mainfrom
GiulioSavini:fix/generic-webhook-response-validation

Conversation

@GiulioSavini
Copy link
Copy Markdown
Contributor

@GiulioSavini GiulioSavini commented Apr 12, 2026

Summary

  • Providers like PushPlus always return HTTP 200 even on failure (wrong field name, invalid token, etc.). shoutrrr's generic service only checks the HTTP status code, so Arcane incorrectly reported the notification as sent.
  • Add an optional successBodyContains field to GenericConfig. When set, SendGenericWithTitle bypasses shoutrrr and makes the HTTP call directly, reads the full response body, and returns an error if the expected substring is absent.
  • Existing behaviour is unchanged for users who do not set successBodyContains — they still go through shoutrrr.

Example PushPlus config: set successBodyContains to "code":200 to distinguish a real success (body contains {"code":200,...}) from a failure (body contains {"code":900,...}).

Closes #2352

Test plan

  • Configure a Generic webhook for PushPlus with a valid token and successBodyContains: '"code":200'
  • Send a test notification — verify it is delivered and no error is shown
  • Configure with an invalid token
  • Send a test notification — verify an error is returned to the UI (previously showed as success)
  • Configure a standard webhook (no successBodyContains) — verify existing behaviour is unchanged

🤖 Generated with Claude Code

Disclaimer Greptiles Reviews use AI, make sure to check over its work.

To better help train Greptile on our codebase, if the comment is useful and valid Like the comment, if its not helpful or invalid Dislike

To have Greptile Re-Review the changes, mention greptileai.

Greptile Summary

This PR adds an optional successBodyContains field to GenericConfig and a new direct HTTP path in SendGenericWithTitle to handle providers (e.g. PushPlus) that always return HTTP 200 but signal failure in the response body. Existing behaviour is fully preserved when the field is not set.

Two new unexported helpers (sendGenericDirect, resolveWebhookURL) are missing the project-required Internal suffix, and resolveWebhookURL duplicates URL-normalisation logic already present in BuildGenericURL.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/naming issues that do not affect runtime behaviour.

No P0 or P1 issues found. The logic is sound, backward-compatible, and the two remaining findings are naming-convention violations and a code-duplication note.

backend/pkg/utils/notifications/generic_sender.go — naming convention violations on the two new private helpers.

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: backend/pkg/utils/notifications/generic_sender.go
Line: 161

Comment:
**Unexported functions missing "Internal" suffix**

Both new unexported functions — `sendGenericDirect` (line 161) and `resolveWebhookURL` (line 231) — are missing the required `Internal` suffix for private helpers.

```suggestion
func sendGenericDirectInternal(ctx context.Context, config models.GenericConfig, title, message string) error {
```

**Rule Used:** What: All unexported functions must have the "Inte... ([source](https://app.greptile.com/review/custom-context?memory=306fc233-4d2f-4ac4-bdf7-8059588e8a43))

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: backend/pkg/utils/notifications/generic_sender.go
Line: 231

Comment:
**"Internal" suffix required for unexported helper**

Same naming rule applies here — the private helper should be `resolveWebhookURLInternal`.

```suggestion
func resolveWebhookURLInternal(config models.GenericConfig) (*url.URL, error) {
```

**Rule Used:** What: All unexported functions must have the "Inte... ([source](https://app.greptile.com/review/custom-context?memory=306fc233-4d2f-4ac4-bdf7-8059588e8a43))

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: backend/pkg/utils/notifications/generic_sender.go
Line: 229-261

Comment:
**URL-parsing logic duplicated from `BuildGenericURL`**

`resolveWebhookURL` reproduces the same scheme-normalisation and host-validation logic that already exists in `BuildGenericURL` (lines 25–45). If the normalisation rules ever diverge, the two code paths will behave differently for the same `WebhookURL` value. Consider extracting the shared logic into one helper and calling it from both `BuildGenericURL` and `sendGenericDirect`.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix: detect provider-level failures in g..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

Context used:

  • Rule used - What: All unexported functions must have the "Inte... (source)

…etarcaneapp#2352)

Providers like PushPlus always return HTTP 200 even when the
notification fails (e.g. wrong field name, invalid token). shoutrrr
only inspects the HTTP status code, so the UI incorrectly reported
"sent" while nothing was delivered.

Add an optional SuccessBodyContains field to GenericConfig. When set,
SendGenericWithTitle bypasses shoutrrr and makes the HTTP call
directly, capturing the full response body. If the body does not
contain the expected substring the send is reported as an error.

Example PushPlus config: set successBodyContains to "\"code\":200"
to distinguish a real success from the {"code":900,...} failure body.
@kmendell
Copy link
Copy Markdown
Member

kmendell commented Apr 12, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

// sendGenericDirect makes the webhook HTTP call directly, giving access to the
// response body so that provider-level success/failure can be detected even
// when the HTTP status is always 200.
func sendGenericDirect(ctx context.Context, config models.GenericConfig, title, message string) error {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unexported functions missing "Internal" suffix

Both new unexported functions — sendGenericDirect (line 161) and resolveWebhookURL (line 231) — are missing the required Internal suffix for private helpers.

Suggested change
func sendGenericDirect(ctx context.Context, config models.GenericConfig, title, message string) error {
func sendGenericDirectInternal(ctx context.Context, config models.GenericConfig, title, message string) error {

Rule Used: What: All unexported functions must have the "Inte... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/pkg/utils/notifications/generic_sender.go
Line: 161

Comment:
**Unexported functions missing "Internal" suffix**

Both new unexported functions — `sendGenericDirect` (line 161) and `resolveWebhookURL` (line 231) — are missing the required `Internal` suffix for private helpers.

```suggestion
func sendGenericDirectInternal(ctx context.Context, config models.GenericConfig, title, message string) error {
```

**Rule Used:** What: All unexported functions must have the "Inte... ([source](https://app.greptile.com/review/custom-context?memory=306fc233-4d2f-4ac4-bdf7-8059588e8a43))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex


// resolveWebhookURL parses and normalises the configured webhook URL,
// adding a default scheme when the user omitted one.
func resolveWebhookURL(config models.GenericConfig) (*url.URL, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 "Internal" suffix required for unexported helper

Same naming rule applies here — the private helper should be resolveWebhookURLInternal.

Suggested change
func resolveWebhookURL(config models.GenericConfig) (*url.URL, error) {
func resolveWebhookURLInternal(config models.GenericConfig) (*url.URL, error) {

Rule Used: What: All unexported functions must have the "Inte... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/pkg/utils/notifications/generic_sender.go
Line: 231

Comment:
**"Internal" suffix required for unexported helper**

Same naming rule applies here — the private helper should be `resolveWebhookURLInternal`.

```suggestion
func resolveWebhookURLInternal(config models.GenericConfig) (*url.URL, error) {
```

**Rule Used:** What: All unexported functions must have the "Inte... ([source](https://app.greptile.com/review/custom-context?memory=306fc233-4d2f-4ac4-bdf7-8059588e8a43))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex

Comment on lines +229 to +261
// resolveWebhookURL parses and normalises the configured webhook URL,
// adding a default scheme when the user omitted one.
func resolveWebhookURL(config models.GenericConfig) (*url.URL, error) {
parsed, err := url.Parse(config.WebhookURL)
if err != nil {
return nil, fmt.Errorf("invalid webhook URL: %w", err)
}

hasScheme := strings.Contains(config.WebhookURL, "://")
if parsed.Host == "" && !hasScheme {
scheme := "https"
if config.DisableTLS {
scheme = "http"
}
normalized := strings.TrimPrefix(config.WebhookURL, "//")
parsed, err = url.Parse(fmt.Sprintf("%s://%s", scheme, normalized))
if err != nil {
return nil, fmt.Errorf("invalid webhook URL: %w", err)
}
}

if parsed.Host == "" {
return nil, fmt.Errorf("invalid webhook URL: missing host")
}

switch strings.ToLower(parsed.Scheme) {
case "http", "https":
default:
return nil, fmt.Errorf("invalid webhook URL scheme: %s", parsed.Scheme)
}

return parsed, nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 URL-parsing logic duplicated from BuildGenericURL

resolveWebhookURL reproduces the same scheme-normalisation and host-validation logic that already exists in BuildGenericURL (lines 25–45). If the normalisation rules ever diverge, the two code paths will behave differently for the same WebhookURL value. Consider extracting the shared logic into one helper and calling it from both BuildGenericURL and sendGenericDirect.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/pkg/utils/notifications/generic_sender.go
Line: 229-261

Comment:
**URL-parsing logic duplicated from `BuildGenericURL`**

`resolveWebhookURL` reproduces the same scheme-normalisation and host-validation logic that already exists in `BuildGenericURL` (lines 25–45). If the normalisation rules ever diverge, the two code paths will behave differently for the same `WebhookURL` value. Consider extracting the shared logic into one helper and calling it from both `BuildGenericURL` and `sendGenericDirect`.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

@kmendell kmendell merged commit a42238c into getarcaneapp:main Apr 12, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐞 Bug: Generic Webhook fails to deliver to PushPlus in v1.17.3 (UI says success, no message received)

2 participants